package top.mcwebsite.lib.gradle.plugin.transform

import java.net.URL
import java.nio.charset.StandardCharsets
import java.security.Provider.Service
import java.util.ServiceConfigurationError

interface ServiceLoader<T> {
    fun load(vararg args: Any): List<T>

    companion object {
        inline fun <reified T> ClassLoader.newServiceLoader(
            vararg types: Class<*>
        ): ServiceLoader<T> {
            return newServiceLoader(this, T::class.java, *types)
        }

        fun <T> newServiceLoader(classLoader: ClassLoader, service: Class<T>, vararg types: Class<*>): ServiceLoader<T> {
            return ServiceLoaderImpl(classLoader, service, *types)
        }
    }
}

private class ServiceLoaderImpl<T>(
    private val classLoader: ClassLoader,
    private val service: Class<T>,
    private vararg val types: Class<*>,
) : ServiceLoader<T> {

    private val name = "META-INF/services/${service.name}"

    override fun load(vararg args: Any): List<T> =
        classLoader.getResources(name)
            ?.asSequence()
            ?.map(::parse)
            ?.flatten()
            ?.toSet()
            ?.mapNotNull {
                it.toClassOrNull(classLoader, service)
            }
            ?.toSet()
            ?.map {
                it.newInstance(*args)
            } ?: emptyList()

    private fun Class<T>.newInstance(vararg args: Any): T =
        try {
            try {
                this.getConstructor(*types).newInstance(*args)
            } catch (e: NoSuchMethodException) {
                this.getDeclaredConstructor().newInstance() as T
            }
        } catch (e: Throwable) {
            e.printStackTrace()
            throw ServiceConfigurationError("Provider $this not found.")
        }

}

private fun parse(url: URL): List<String> = try {
    url.openStream()
        .bufferedReader(StandardCharsets.UTF_8)
        .readLines()
        .filter { line ->
            line.isNotEmpty() && line.isNotBlank() && !line.startsWith("#")
        }
        .map(String::trim)
        .filter(::isJavaClassName)
} catch (e: Throwable) {
    emptyList()
}

private fun isJavaClassName(text: String): Boolean {
    if (!Character.isJavaIdentifierPart(text[0])) {
        throw ServiceConfigurationError("Illegal provider-class name: text")
    }

    for (i in 1 until text.length) {
        val cp = text.codePointAt(i)
        if (!Character.isJavaIdentifierPart(cp) && cp != '.'.code) {
            throw ServiceConfigurationError("Illegal provider-class name: $text")
        }
    }
    return true
}

private fun <T> String.toClassOrNull(classLoader: ClassLoader, service: Class<T>) = try {
    val provider = this
    val providerClass = Class.forName(provider, false, classLoader)
    if (!service.isAssignableFrom(providerClass)) {
        throw ServiceConfigurationError("Provider $provider not a subtype")
    }
    providerClass as Class<T>
} catch (e: Throwable) {
    null
}